package edu.hawaii.ics414.blackjack.player;

import edu.hawaii.ics414.blackjack.card.Card;
import edu.hawaii.ics414.blackjack.card.GameDeck;
import java.util.HashMap;
import java.util.Map;

/**
 * The most skilled of the AIs provided in this game.
 * Counts all cards played.
 * 
 * @author Branden Ogata
 *
 */

public class ProPlayer extends ExperiencedPlayer implements CardCounter
{  
  /**
   * The number of cards of each value left in the deck.
   * 
   */
  private HashMap<Integer, Integer> counts;
  
  /**
   * The number of decks in play.
   * 
   */
  private int decks;
  
  /**
   * The lowest bet for this ProPlayer.
   * 
   */
  private static int LOW_BET = 10;
  
  /**
   * The medium bet for this ProPlayer;
   * 
   */
  private static int MEDIUM_BET = 20;
  
  /**
   * The high bet for this ProPlayer.
   * 
   */
  private static int HIGH_BET = 40;
  
  /**
   * Creates a new ProPlayer.
   * 
   * @param money The int equal to the starting amount of money for the new SemiProPlayer.
   * @param cards The int equal to the number of cards in the deck.
   * 
   */
  
  public ProPlayer(int money, int decks)
  {
    super(money);
    this.decks = decks;
    
    this.counts = new HashMap<Integer, Integer>();
    this.counts.put(1, 4 * decks);
    this.counts.put(2, 4 * decks);
    this.counts.put(3, 4 * decks);
    this.counts.put(4, 4 * decks);
    this.counts.put(5, 4 * decks);
    this.counts.put(6, 4 * decks);
    this.counts.put(7, 4 * decks);
    this.counts.put(8, 4 * decks);
    this.counts.put(9, 4 * decks);
    this.counts.put(10, 16 * decks);
  }

  /**
   * Returns the move of this ProPlayer.
   * 
   * @param deck The GameDeck containing the cards to use.
   * @param hand The int indicating the hand to play on.
   * @param hole The Card that the dealer has showing.
   * 
   * @return action The Action that this ProPlayer intends to play.
   * 
   */
  
  @Override
  public Action getMove(GameDeck deck, int hand, Card hole)
  {
    Action action = super.getMove(deck, hand, hole);
    
    // If the action is to hit, stand, or double down, then consider probabilities
    // (split should occur regardless since something like eight-pair should be split all the time)
    if (!action.equals(Action.SPLIT))
    {
      // If the probability of busting is greater than 40%, then stand
      if (((double) safeCards(this.sum(hand)) / (double) cardTotal()) > 0.40)
      {
        action = Action.STAND;
      }
      // Else decide whether to double down or hit
      else
      {
        // If the dealer has a 60% or greater chance of busting, then double down if legal
        if ((this.canDoubleDown(hand)) && (((double) safeCards(hole.getValue() + 
                                                               this.averageValue()) / 
                                            (double) cardTotal()) > 0.60))
        {
          action = Action.DOUBLE_DOWN;
        }
        // Else hit
        else if (this.canHit(hand))
        {
          action = Action.HIT;
        }
        // Else stand if none of the above apply
        else
        {
          action = Action.STAND;
        }
      }
    }
    
    return action;
  }  

  /**
   * Starts a new hand with an initial bet depending on the current count and last round.
   * 
   * @param deck The GameDeck to take cards from.
   * 
   */
  
  @Override
  public void startHand(GameDeck deck)
  {
    int count = 0;
    int bet = this.lastBet[0];
    
    // Increase count by one for each card of value 3, 4, 5, 6
    for (int i = 3; i <= 6; i++)
    {
      count += (4 * this.decks) - this.counts.get(i);
    }
    
    // Decrease count by one for each card of value 10
    count -= (16 * decks) - this.counts.get(10);
    
    // If count is positive and last round was a win
    if ((count > 0) && (this.lastRound == 1))
    {
      // Increase bet to the next level (unless already at HIGH_BET)
      if (bet == LOW_BET)
      {
        bet = MEDIUM_BET;
      }
      else if (bet == MEDIUM_BET)
      {
        bet = HIGH_BET;
      }
    }
    
    // If count is positive and last round was a loss
    else if ((count > 0) && (this.lastRound == -1))
    {
      // Increase bet if at LOW_BET
      if (bet == LOW_BET)
      {
        bet = MEDIUM_BET;
      }
    }
    
    // If count is negative and last round was a win, do not change bet 
    // since decreasing after a win would be suspicious
    
    // If count is negative and last round was a loss
    if ((count < 0) && (this.lastRound == -1))
    {
      // Decrease bet to the previous level (unless already at LOW_BET)
      if (bet == MEDIUM_BET)
      {
        bet = LOW_BET;
      }
      else if (bet == HIGH_BET)
      {
        bet = MEDIUM_BET;
      }
    }
    
    this.setBet(0, bet);
    super.startHand(deck);
  }
  
  @Override
  public void updateCount(Card card)
  {
    this.counts.put(card.getValue(), this.counts.get(card.getValue()) - 1);
  }

  @Override
  public void resetCount()
  {
    this.counts.put(1, 4 * decks);
    this.counts.put(2, 4 * decks);
    this.counts.put(3, 4 * decks);
    this.counts.put(4, 4 * decks);
    this.counts.put(5, 4 * decks);
    this.counts.put(6, 4 * decks);
    this.counts.put(7, 4 * decks);
    this.counts.put(8, 4 * decks);
    this.counts.put(9, 4 * decks);
    this.counts.put(10, 16 * decks);
  }

  @Override
  public CardCounter getCardCounter()
  {
    return (CardCounter) this;
  }
  
  /**
   * Calculates the number of safe cards in the deck given the current value of a hand.
   * 
   * @param sum The double equal to the current or approximated sum of all cards in a hand.
   * 
   * @return safe The int equal to the number of cards that will not cause the hand to bust.
   * 
   */
  
  private int safeCards(double sum)
  {
    int safe = 0;

    for (Map.Entry<Integer, Integer> cardValue : this.counts.entrySet())
    {
      if (cardValue.getKey() + sum <= 21)
      {
        safe += cardValue.getValue();
      }
    }
    
    return safe;
  }

  /**
   * Calculates the total number of cards left in the deck.
   * 
   * @return total The int equal to the number of cards in the deck.
   * 
   */
  
  private int cardTotal()
  {
    int total = 0;
    
    // For each value, add to the total
    for (Integer i : this.counts.values())
    {
      total += i;
    }
    
    return total;
  }
  
  /**
   * Returns the average value of the cards remaining in the deck.
   * 
   * @return average The double equal to the mean of the cards left.
   * 
   */
  
  private double averageValue()
  {
    double average = 0;
    
    for (Map.Entry<Integer, Integer> cardValue : this.counts.entrySet())
    {
      average += cardValue.getKey() * cardValue.getValue();
    }
    
    average /= (double) this.cardTotal();
    
    return average;
  }
}
